Skip to content

feat(devnet): composable local devnet with scenarios, Docker image, and CI#7

Merged
Quantumlyy merged 30 commits into
masterfrom
devnet
May 31, 2026
Merged

feat(devnet): composable local devnet with scenarios, Docker image, and CI#7
Quantumlyy merged 30 commits into
masterfrom
devnet

Conversation

@Quantumlyy

@Quantumlyy Quantumlyy commented May 30, 2026

Copy link
Copy Markdown
Member

Summary

Adds a clean, modern local devnet for the EFP contracts — inspired by ENS's runDevnet.ts but lighter and built around composable scenarios that are usable directly from tests. It can also deploy EFP onto an already-running node (e.g. the ENS devnet) so both stacks share one chain.

Importantly, no existing deploy files are modifieddeploy.s.sol, mint.s.sol, Deployer.sol, and foundry.toml are untouched, so the live production deployments remain reproducible. Deployed addresses are read from Forge's standard broadcast/ artifact instead.

What's included

  • setupDevnet() (scripts/devnet/setup.ts) — boots a devnet and returns an environment: viem clients, named accounts, bound contract instances, and snapshot/revert/mine helpers. Spawns anvil via prool, or attaches to an existing node when given an rpcUrl.
  • Composable scenarios (scripts/devnet/scenarios.ts) — small ops (openPublicMint, mintList, follow, tag) plus presets (empty, minimal, demoGraph) selectable via --scenario.
  • CLI (scripts/runDevnet.ts) — deploy, seed a scenario, print accounts + addresses, and serve a /health endpoint.
  • DockerDockerfile.devnet, compose.yml (standalone) and compose.attach.yml (EFP deployed onto the ENS devnet on one chain).
  • CI.github/workflows/build-devnet.yml builds the image, smoke-tests it via /health, and publishes to ghcr.io/ethereumfollowprotocol/contracts/devnet.
  • Example tests (scripts/devnet/devnet.test.ts) demonstrating the setup → scenario → snapshot/revert pattern.

Usage

bun run devnet                 # standalone, empty chain
bun run devnet:seed            # standalone + demoGraph scenario
bun run devnet:attach          # deploy EFP onto a node at http://127.0.0.1:8545
bun run devnet:test            # run the example integration tests

docker compose up              # standalone container
docker compose -f compose.attach.yml up   # alongside the ENS devnet

Copilot AI review requested due to automatic review settings May 30, 2026 14:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Bun/Foundry-powered local EFP devnet with composable scenarios, Docker support, and CI image smoke testing.

Changes:

  • Introduces setupDevnet(), devnet clients/accounts/deployment helpers, scenario utilities, and example Bun integration tests.
  • Adds runDevnet.ts CLI with health endpoint plus standalone and ENS-attach Docker Compose configurations.
  • Adds a devnet Dockerfile and GitHub Actions workflow for building/smoke-testing/publishing the image.

Reviewed changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
scripts/runDevnet.ts CLI entrypoint for starting, seeding, printing, and health-serving the devnet.
scripts/devnet/* New devnet setup, deployment, encoding, scenarios, clients, accounts, shutdown, and tests.
Dockerfile.devnet Builds the devnet runtime image.
compose.yml Replaces old two-service local setup with single devnet service.
compose.attach.yml Adds ENS-devnet attach mode.
.github/workflows/build-devnet.yml Adds image build, smoke test, and publish workflow.
package.json Adds devnet scripts/dependency and updates package metadata.
.editorconfig, .dockerignore, deployments/.gitkeep Updates formatting/docker context support and deployment output directory.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/build-devnet.yml
Comment thread scripts/devnet/shutdown.ts Outdated
Comment thread package.json
Quantumlyy and others added 2 commits May 30, 2026 16:50
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@socket-security

socket-security Bot commented May 30, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedprool@​0.2.48710010085100

View full report

Bun 1.x transitioned from a binary `bun.lockb` to a human-readable `bun.lock` file. This updates the project to use the new text-based lockfile format and adjusts the CI workflow accordingly for better diffing and consistency.
*   Aligns both `Dockerfile` and `Dockerfile.devnet` to use the new `bun.lock` file for dependency installation.
*   Reflects Bun's transition from the binary `bun.lockb` to the human-readable `bun.lock` format.
*   Includes minor formatting adjustments for `RUN` commands.
*   Adds an exception to `.dockerignore` to prevent `bun.lock` from being excluded from the Docker build context.
*   This is necessary as `bun.lock` has replaced `bun.lockb` as the standard lockfile for Bun 1.x.
*   Ensures Docker builds can properly utilize the new lockfile format for consistent dependency management, aligning with recent Dockerfile updates.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 55 changed files in this pull request and generated 3 comments.

Comment thread scripts/devnet/scenarios.ts Outdated
Comment thread scripts/runDevnet.ts Outdated
Comment thread .github/workflows/build-devnet.yml
Only run the devnet build workflow on pull requests when changes are detected in source code, scripts, or libraries. This optimizes CI resource usage by preventing unnecessary builds for changes in unrelated files.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 55 changed files in this pull request and generated 1 comment.

Comment thread src/EFPListRegistry.sol Outdated

@encryptedDegen encryptedDegen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work putting this devnet together! The setupDevnet utility and composable scenarios are very clean, and having a Dockerized version makes it extremely easy for frontend or indexer work.

Here is the feedback man. The most critical one is regarding bytecode reproducibility (which Copilot also flagged but wasn't addressed, or maybe it was, idk).

1. Bytecode Reproducibility (SPDX changes)

The PR description mentions that because deploy.s.sol and other deploy scripts are untouched, "the live production deployments remain reproducible".
However, changing the SPDX-License-Identifier from UNLICENSED to MIT in all .sol files actually modifies the compiled bytecode. Because foundry.toml does not set bytecode_hash = "none", the Solidity compiler appends the IPFS metadata hash (which is derived from the exact source file contents, including comments) to the bytecode.
If you need strict, byte-for-byte reproducibility with the existing production deployments (e.g. for Etherscan verification using the exact repo commit), you will either need to revert the SPDX comment changes or acknowledge that the new bytecode artifacts will no longer match the live deployments.

2. Catch unhandledRejection for graceful shutdown

In scripts/devnet/shutdown.ts, you gracefully handle uncaughtException. It would be a good idea to also handle unhandledRejection. For instance, if waitForNode() in setup.ts times out, it throws an async error that will skip your shutdown hooks and leave the process hanging or crashing ungracefully.

process.once('unhandledRejection', async (reason) => {
  console.error(reason)
  await shutdown('unhandledRejection', 1)
})

3. CI integration for devnet.test.ts

The PR introduces a great integration test in scripts/devnet/devnet.test.ts, but it looks like .github/workflows/test.yml only runs forge test. You should consider adding a step to install bun and run bun run devnet:test in your test.yml to prevent regressions in the devnet tooling!

4. Minor parseArgs boolean behavior

Just a small heads up on parseArgs for save-deployments in runDevnet.ts. Node's parseArgs doesn't natively parse --save-deployments=false as a boolean false (it typically requires passing --no-save-deployments with specific config, or relying purely on the env var). The current setup where DEVNET_SAVE_DEPLOYMENTS=false overrides it is perfectly fine, just something to keep in mind if someone tries to disable it via CLI arguments.

*   add a dedicated CI job to automate devnet integration tests
*   enhance devnet stability by handling unhandled promise rejections during shutdown
*   introduce `--no-save-deployments` flag for `runDevnet.ts`, offering clearer control over deployment persistence
@Quantumlyy Quantumlyy requested a review from encryptedDegen May 31, 2026 09:07
@Quantumlyy

Copy link
Copy Markdown
Member Author

@greptileai

@greptile-apps

greptile-apps Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Introduces a self-contained local devnet for EFP contracts, including composable scenarios, a CLI runner, Docker support, and CI integration — without touching any existing deploy scripts or production artifacts.

  • setupDevnet() / scenarios — boots anvil via prool (or attaches to an external node), runs forge script deploy.s.sol, parses broadcast artifacts for addresses, and exposes snapshot/revert helpers. Composable scenario primitives (openPublicMint, mintList, follow, tag) plus three presets (empty, minimal, demoGraph) can be used directly in integration tests.
  • Docker / CIDockerfile.devnet pins Foundry to v1.7.1 via ARG FOUNDRY_VERSION, compose.yml covers the standalone case, compose.attach.yml deploys EFP onto a shared ENS devnet chain, and build-devnet.yml builds, smoke-tests, and publishes the image to GHCR on pushes to master.

Confidence Score: 5/5

Safe to merge — this is a purely additive devnet tooling layer with no changes to production deploy scripts or contract source.

All issues raised in earlier review rounds have been addressed: the push trigger targets master, the anvil double-stop is gone, Foundry is pinned via ARG FOUNDRY_VERSION, the private key travels only through the environment variable, and a try/finally in setupDevnet stops the anvil process on deploy failure. The encoding helpers are verified against the Solidity decodeL1ListStorageLocation layout. The remaining observations are documentation gaps rather than defects.

No files require special attention; scenarios.ts has a minor documentation gap around mintList when to and manager differ, but all current callers pass them as the same account.

Important Files Changed

Filename Overview
scripts/devnet/setup.ts Core setup function: boots anvil or attaches to existing node, deploys contracts, builds environment. Try/finally correctly cleans up anvil on deploy failure.
scripts/devnet/scenarios.ts Composable devnet scenarios and presets. MintState comparison is safe (uint8 → number in viem). mintList's to param has a subtle contract-level asymmetry worth documenting.
scripts/devnet/deploy.ts Runs forge deploy and parses broadcast artifact for addresses. Private key correctly passed via env var, never as CLI arg.
scripts/devnet/anvil.ts Starts anvil via prool. Previous double-stop issue addressed; only stopFn() is called in the returned handle.
scripts/devnet/encoding.ts TypeScript mirrors of on-chain EFP encoding helpers. Structure matches the Solidity decodeL1ListStorageLocation layout (86 bytes). All correct.
scripts/devnet/shutdown.ts Process signal handlers and finalizer registry. Promise.allSettled ensures all finalizers run.
scripts/runDevnet.ts CLI entry point: parses args, runs scenario, prints table, serves health endpoint. Health server binds correctly to DEVNET_HOST.
scripts/devnet/devnet.test.ts Integration tests demonstrating setup → scenario → snapshot/revert pattern. Snapshot is correctly re-taken after each revert.
.github/workflows/build-devnet.yml Builds and smoke-tests the devnet Docker image, then pushes to GHCR on non-PR events. Push trigger correctly targets master.
Dockerfile.devnet Bun 1.2.13 base image with Foundry pinned via ARG FOUNDRY_VERSION=v1.7.1. Previous reproducibility concern addressed.
scripts/devnet/client.ts Creates a viem test client combining public + wallet + test actions. All four EFP contracts are bound using ABI from generated/abi.ts.
compose.attach.yml Compose file for deploying EFP onto the ENS devnet. efp-devnet depends on devnet health check. efp-deployments volume correctly captures addresses.

Sequence Diagram

sequenceDiagram
    participant CLI as runDevnet.ts
    participant Setup as setupDevnet()
    participant Anvil as prool/anvil
    participant Forge as forge script
    participant Broadcast as broadcast artifact
    participant Scenario as scenario fn
    participant Health as HTTP /health

    CLI->>Setup: setupDevnet(options)
    alt standalone mode
        Setup->>Anvil: "startAnvil({ port, chainId, ... })"
        Anvil-->>Setup: "AnvilHandle { rpcUrl, stop }"
    else attach mode
        Setup->>Setup: waitForNode(externalRpcUrl)
    end
    Setup->>Forge: bun spawn forge script deploy.s.sol --broadcast
    Forge-->>Broadcast: broadcast/deploy.s.sol/chainId/run-latest.json
    Setup->>Broadcast: readDeploymentsFromBroadcast(chainId)
    Broadcast-->>Setup: Deployments (addresses)
    Setup->>Setup: getContracts(client, deployments)
    Setup-->>CLI: DevnetEnvironment
    CLI->>Scenario: scenarios[name](env)
    Scenario->>Scenario: openPublicMint / mintList / follow / tag
    CLI->>Health: httpServer.listen(healthPort)
    CLI->>CLI: keepAlive() [never resolves]
    CLI-->>Health: GET /health → 200 healthy
Loading

Fix All in Conductor Fix All in Cursor Fix All in Codex Fix All in Claude Code

Reviews (3): Last reviewed commit: "fix(devnet): stop anvil on setup failure" | Re-trigger Greptile

Comment thread .github/workflows/build-devnet.yml
Comment thread scripts/devnet/anvil.ts
Comment thread Dockerfile.devnet Outdated
Comment thread scripts/devnet/deploy.ts Outdated
Comment thread scripts/devnet/setup.ts Outdated
ensure the Anvil instance is stopped if an error occurs during the devnet setup process, preventing orphaned processes.
@Quantumlyy Quantumlyy merged commit c76225c into master May 31, 2026
9 checks passed
@Quantumlyy Quantumlyy deleted the devnet branch May 31, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants